בידול אוטומטי מתקדם

צפה ב- TensorFlow.org הפעל ב- Google Colab צפה במקור ב- GitHub הורד מחברת

מבוא הדרגתיים והבחנה אוטומטית המדריך כולל את כל הנדרש כדי הדרגתיים לחשב TensorFlow. מדריך זה מתמקד, תכונות משותפות פחות עמוקות של tf.GradientTape API.

להכין

pip uninstall tensorflow keras -y
pip install tf-nightly
import tensorflow as tf

import matplotlib as mpl
import matplotlib.pyplot as plt

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

שליטה בהקלטת שיפוע

בשנות ה מדריך הבידול האוטומטי שראית כיצד לקבוע באיזה משתנה tensors נצפים על ידי הקלטת תוך בניית חישוב השיפוע.

לקלטת יש גם שיטות לתמרן את ההקלטה.

הפסק להקליט

אם ברצונך לעצור את ההקלטה הדרגתית, אתה יכול להשתמש 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 להקים בעצמך (עיין Docs 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 Docs 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 " דפוס טוב בזמן שמחשבי סקלר מן שיפוע, ואז סקלר וכתוצאה מתנהג כמקור עבור חישוב שיפוע שני, כמו בדוגמא הבאה.

דוגמה: ויסות שיפוע קלט

מודלים רבים רגישים ל"דוגמאות מנוגדות ". אוסף טכניקות זה משנה את קלט המודל כדי לבלבל את תפוקת המודל. היישום-כגון פשוט כמו למשל תשובה: באמצעות Gradient Fast חתום התקפת שיטת -takes צעד אחד לאורך השיפוע של התפוקה ביחס הקלט; "שיפוע הקלט".

אחת טכניקות להגדיל חוסן לדוגמה יריבות היא הסדרת שיפוע קלט (פינלי & אוברמן, 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 : The sources הטיעון יכול להיות מותח או מיכל של טנזורים.
  • בניגוד gradient : The 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])

צורת היעקוביאן של הפלט ביחס לגרעין היא שתי הצורות המשולבות יחד:

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: 4.7683716e-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 , יישום זה למודל הלא טריוויאלי ידרוש שרשור ולשיסוע מקפיד לייצר הסי מלא על פני משתנים מרובים.

אצווה ג'ייקוביאנית

במקרים מסוימים, אתה רוצה לקחת את הג'ייקוביאני מכל ערימת מטרות ביחס לערמת מקורות, כאשר היעקובנים לכל זוג יעד-מקור הם עצמאיים.

לדוגמה, כאן קלט 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 0x7f96f419b8c0> 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 0x7f96e8655ef0> 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)