روز جامعه ML 9 نوامبر است! برای به روز رسانی از TensorFlow، JAX به ما بپیوندید، و بیشتر بیشتر بدانید

تمایز اتوماتیک پیشرفته

مشاهده در TensorFlow.org در Google Colab اجرا کنید مشاهده منبع در GitHub دانلود دفترچه یادداشت

مقدمه به شیب و تمایز اتوماتیک راهنما شامل همه چیز مورد نیاز به شیب محاسبه در TensorFlow. این راهنما در عمیق تر، ویژگی های کمتر رایج از تمرکز tf.GradientTape API.

برپایی

import tensorflow as tf

import matplotlib as mpl
import matplotlib.pyplot as plt

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

کنترل ضبط گرادیان

در راهنمای تمایز خودکار شما را دیدم که چگونه به کنترل که متغیرها و تانسورها توسط نوار تماشا در حالی که ساختمان محاسبه شیب.

این نوار همچنین دارای روش هایی برای دستکاری ضبط است.

ضبط را متوقف کنید

اگر شما مایل به توقف ضبط شیب، شما می توانید استفاده tf.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

تنظیم مجدد/شروع ضبط از ابتدا

اگر شما مایل به بیش از شروع به طور کامل، استفاده از tf.GradientTape.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

شیب های سفارشی

در برخی موارد ، ممکن است بخواهید دقیقاً نحوه محاسبه گرادیان ها را بجای استفاده از پیش فرض کنترل کنید. این شرایط عبارتند از:

  1. برای عملیات جدیدی که می نویسید گرادیان مشخصی وجود ندارد.
  2. محاسبات پیش فرض از نظر عددی ناپایدار هستند.
  3. شما مایل به محاسبه گران قیمت از پاس رو به جلو هستید.
  4. می خواهید تغییر دهید یک مقدار (به عنوان مثال، با استفاده از tf.clip_by_value یا tf.math.round ) بدون تغییر شیب.

برای مورد اول، برای نوشتن یک عملیات جدید شما می توانید با استفاده از tf.RegisterGradient به راه اندازی خود را (اشاره به اسناد API برای جزئیات بیشتر). (توجه داشته باشید که رجیستری گرادیان جهانی است ، بنابراین با احتیاط آن را تغییر دهید.)

برای دومی سه مورد، شما می توانید با استفاده از 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 برای جزئیات بیشتر اسناد دکوراتور API.

شیب های سفارشی در SavedModel

شیب سفارشی را می توان به SavedModel با استفاده از گزینه ذخیره tf.saved_model.SaveOptions(experimental_custom_gradients=True) .

می شود را به SavedModel را نجات داد، تابع شیب باید قابل ردیابی می شود (برای کسب اطلاعات بیشتر، لطفا از عملکرد بهتر با tf.function راهنمای).

class MyModule(tf.Module):

  @tf.function(input_signature=[tf.TensorSpec(None)])
  def call_custom_grad(self, x):
    return clip_gradients(x)

model = MyModule()
tf.saved_model.save(
    model,
    'saved_model',
    options=tf.saved_model.SaveOptions(experimental_custom_gradients=True))

# The loaded gradients will be the same as the above example.
v = tf.Variable(2.0)
loaded = tf.saved_model.load('saved_model')
with tf.GradientTape() as t:
  output = loaded.call_custom_grad(v * v)
print(t.gradient(output, v))
INFO:tensorflow:Assets written to: saved_model/assets
tf.Tensor(2.0, shape=(), dtype=float32)

توجه داشته باشید در مورد مثال بالا: اگر شما سعی کنید به جای کد بالا را با tf.saved_model.SaveOptions(experimental_custom_gradients=False) ، گرادیان هنوز هم همان نتیجه را تولید در حال بارگذاری خواهد شد. دلیل آن این است که رجیستری شیب هنوز شامل شیب سفارشی مورد استفاده در تابع call_custom_op . حال، اگر شما در زمان اجرا راه اندازی مجدد پس از صرفه جویی بدون شیب سفارشی، در حال اجرا مدل لود تحت tf.GradientTape خطا پرتاب: LookupError: No gradient defined for operation 'IdentityN' (op type: IdentityN) .

نوارهای متعدد

نوارهای متعدد به طور یکپارچه با یکدیگر ارتباط برقرار می کنند.

به عنوان مثال ، در اینجا هر نوار مجموعه ای از تنسورهای مختلف را مشاهده می کند:

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

شیب درجه بالاتر

عملیات داخل tf.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

در حالی که نشان می دهد که شما در مشتق دوم تابع عددی می دهد، این الگو را تعمیم داد به تولید یک ماتریس بلند، از tf.GradientTape.gradient تنها محاسبه گرادیان یک عددی. برای ساخت یک ماتریس هشین ، به رفتن به عنوان مثال بلند تحت بخش ژاکوبین .

"تو در تو را به tf.GradientTape.gradient " یک الگوی خوب زمانی که شما در حال محاسبه یک اسکالر از یک شیب، و پس از آن اسکالر در نتیجه به عنوان یک منبع برای یک محاسبه گرادیان دوم عمل می کند، همانطور که در مثال زیر است.

مثال: منظم سازی گرادیان ورودی

بسیاری از مدلها مستعد "مثالهای مخالف" هستند. این مجموعه از تکنیک ها ، ورودی مدل را تغییر می دهد تا خروجی مدل دچار اشتباه شود. ساده ترین پیاده سازی مانند مثال خصمانه با استفاده از گرادیان سریع امضا حمله به روش طول می کشد یک قدم در امتداد شیب خروجی با توجه به ورودی؛ "گرادیان ورودی"

یک روش برای افزایش استحکام به نمونه های خصمانه است ورودی تنظیم شیب (فینلی و ابرمن، 2019)، که تلاش برای به حداقل رساندن میزان شیب ورودی. اگر گرادیان ورودی کوچک است ، تغییر خروجی نیز باید کوچک باشد.

در زیر پیاده سازی ساده از نظم دهی گرادیان ورودی است. پیاده سازی عبارت است از:

  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])]

یعقوبیان

همه مثالهای قبلی گرادیانهای یک هدف مقیاس را با توجه به برخی از تنسورها (ها) گرفته اند.

ماتریس ژاکوبین نشان دهنده شیب از یک تابع برداری مقدار. هر سطر شامل گرادیان یکی از عناصر بردار است.

tf.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)

هنگامی که شما را ژاکوبین با توجه به یک اسکالر نتیجه به شکل هدف، و گرادیان از هر عنصر با توجه به منبع می دهد:

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

منبع تنسور

آیا ورودی اسکالر یا تانسور است، tf.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])

شکل Jacobian خروجی با توجه به هسته آن دو شکل متصل به هم است:

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

اگر شما جمع بیش از ابعاد مورد نظر، شما به سمت چپ با شیب مجموع که می توانست با محاسبه شده است tf.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: 2.3841858e-07

مثال: حسینی

در حالی که tf.GradientTape کند یک روش صریح و روشن برای ساخت یک نمی دهد ماتریس هشین این امکان وجود دارد برای ساخت یک با استفاده از tf.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)

برای استفاده از این ادم مزدور برای روش نیوتن مرحله، برای بار اول شما پهن محور آن را به یک ماتریس، و پهن کردن شیب را به یک بردار:

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

مرحله به روز رسانی روش نیوتن در زیر نشان داده شده است:

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 ، استفاده از این را به یک مدل غیر بدیهی می الحاق دقیق و برش نیاز برای تولید یک ادم مزدور کامل در سراسر متغیرهای متعدد.

دسته ای یعقوبیان

در برخی موارد ، شما می خواهید Jacobian را از هر یک از مجموعه ای از اهداف با توجه به یک پشته از منابع ، که در آن Jacobians برای هر جفت هدف-منبع مستقل هستند ، بگیرید.

به عنوان مثال، در اینجا ورودی 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 :

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)

انجام محاسبه بدون بعد اضافی در وهله اول بسیار کارآمدتر خواهد بود. tf.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 0x7f7d601250e0> 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/guide/function#controlling_retracing 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 0x7f7cf062fa70> 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/guide/function#controlling_retracing 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}')
jb.shape: (7, 6, 5)