לשמור את התאריך! קלט / פלט של Google חוזר 18-20 במאי הירשם עכשיו
דף זה תורגם על ידי Cloud Translation API.
Switch to English

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

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

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

להכין

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 כאשר היציאה מבלוק הקלטת קשה או בלתי אפשרית.

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

אמנם זה נותן לך את הנגזרת השנייה של פונקציה סקלרית , אך דפוס זה אינו כללי לייצור מטריצה ​​הסית, מכיוון ש- GradientTape.gradient מחשב רק את השיפוע של סקלר. לבניית הסינית, ראו את הדוגמה ההסיאית בסעיף יעקב .

"שיחות מקוננות ל- GradientTape.gradient " הוא דפוס טוב כאשר אתה מחשב סקלר מ- 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])]

יעקוביאנים

כל הדוגמאות הקודמות לקחו את הדרגתיות של יעד סקלרי ביחס למגדלי מקור מסוימים.

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

שיטת 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

מקור טנסור

בין אם הקלט הוא סקלרי או טנזור, 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

דוגמא: הסיאן

בעוד ש-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)

כדי להשתמש בהסיאן זה לצעד השיטה של ​​ניוטון, תחילה תשטח את ציריו למטריצה, ותשטח את השיפוע לווקטור:

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 אחד. tf.Variable , אך יישום זה על מודל לא טריוויאלי ידרוש שרשור 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)

זה יהיה הרבה יותר יעיל לעשות את החישוב בלי המימד הנוסף מלכתחילה. שיטת 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)