مقدمة في التدرجات والتمايز التلقائي

عرض على TensorFlow.org تشغيل في Google Colab عرض المصدر على جيثب تحميل دفتر
التفاضل التلقائي والتدرجات

التمايز التلقائي مفيد لتنفيذ خوارزميات التعلم الآلي مثل backpropagation لتدريب الشبكات العصبية.

في هذا الدليل ، سوف تستكشف طرقًا لحساب التدرجات باستخدام TensorFlow ، خاصةً في التنفيذ الحثيث.

يثبت
import numpy as np
import matplotlib.pyplot as plt

import tensorflow as tf
تدرجات الحوسبة

للتمييز تلقائيًا ، يحتاج TensorFlow إلى تذكر العمليات التي تحدث بأي ترتيب أثناء التمرير الأمامي . بعد ذلك ، أثناء التمرير للخلف ، يجتاز TensorFlow قائمة العمليات هذه بترتيب عكسي لحساب التدرجات اللونية.

أشرطة متدرجة

يوفر tf.GradientTape واجهة برمجة تطبيقات tf.GradientTape للتمايز التلقائي ؛ أي حساب التدرج اللوني لعملية حسابية فيما يتعلق ببعض المدخلات ، عادةً tf.Variable s. TensorFlow "يسجل" العمليات ذات الصلة المنفذة داخل سياق tf.GradientTape على "شريط". ثم يستخدم TensorFlow هذا الشريط لحساب تدرجات الحساب "المسجل" باستخدام تمايز الوضع العكسي .

اليك مثال بسيط:

x = tf.Variable(3.0)

with tf.GradientTape() as tape:
  y = x**2

بمجرد تسجيل بعض العمليات ، استخدم GradientTape.gradient(target, sources) لحساب التدرج اللوني لبعض الأهداف (غالبًا ما تكون خسارة) بالنسبة إلى مصدر ما (غالبًا متغيرات النموذج):

# dy = 2x * dx
dy_dx = tape.gradient(y, x)
dy_dx.numpy()
6.0

يستخدم المثال أعلاه الحجميات ، لكن tf.GradientTape يعمل بسهولة على أي موتر:

w = tf.Variable(tf.random.normal((3, 2)), name='w')
b = tf.Variable(tf.zeros(2, dtype=tf.float32), name='b')
x = [[1., 2., 3.]]

with tf.GradientTape(persistent=True) as tape:
  y = x @ w + b
  loss = tf.reduce_mean(y**2)

للحصول على تدرج loss فيما يتعلق بكلا المتغيرين ، يمكنك تمرير كلاهما كمصادر إلى طريقة gradient . يتسم الشريط بالمرونة فيما يتعلق بكيفية تمرير المصادر وسيقبل أي مجموعة متداخلة من القوائم أو القواميس ويعيد التدرج منظمًا بنفس الطريقة (انظر tf.nest ).

[dl_dw, dl_db] = tape.gradient(loss, [w, b])

التدرج بالنسبة لكل مصدر له شكل المصدر:

print(w.shape)
print(dl_dw.shape)
(3, 2)
(3, 2)

ها هو حساب التدرج مرة أخرى ، وهذه المرة تمرير قاموس للمتغيرات:

my_vars = {
    'w': w,
    'b': b
}

grad = tape.gradient(loss, my_vars)
grad['b']
<tf.Tensor: shape=(2,), dtype=float32, numpy=array([-1.6920902, -3.2363236], dtype=float32)>
التدرجات بالنسبة للنموذج

من الشائع تجميع tf.Variables في وحدة tf.Module أو إحدى فئاتها الفرعية (طبقات ، طبقة ، keras.Model layers.Layer من أجل نقاط التفتيش والتصدير .

في معظم الحالات ، ستحتاج إلى حساب التدرجات فيما يتعلق بمتغيرات النموذج القابلة للتدريب. نظرًا لأن جميع الفئات الفرعية لـ tf.Module تجمع متغيراتها في خاصية Module.trainable_variables ، يمكنك حساب هذه التدرجات في بضعة أسطر من التعليمات البرمجية:

layer = tf.keras.layers.Dense(2, activation='relu')
x = tf.constant([[1., 2., 3.]])

with tf.GradientTape() as tape:
  # Forward pass
  y = layer(x)
  loss = tf.reduce_mean(y**2)

# Calculate gradients with respect to every trainable variable
grad = tape.gradient(loss, layer.trainable_variables)
for var, g in zip(layer.trainable_variables, grad):
  print(f'{var.name}, shape: {g.shape}')
dense/kernel:0, shape: (3, 2)
dense/bias:0, shape: (2,)

السيطرة على ما يشاهده الشريط

السلوك الافتراضي هو تسجيل جميع العمليات بعد الوصول إلى tf.Variable للتدريب. أسباب ذلك هي:

  • يحتاج الشريط إلى معرفة العمليات التي يجب تسجيلها في التمرير الأمامي لحساب التدرجات اللونية في التمرير الخلفي.
  • يحتوي الشريط على مراجع لمخرجات وسيطة ، لذلك لا تريد تسجيل العمليات غير الضرورية.
  • تتضمن حالة الاستخدام الأكثر شيوعًا حساب التدرج اللوني للخسارة فيما يتعلق بجميع متغيرات النموذج القابلة للتدريب.

على سبيل المثال ، يفشل ما يلي في حساب التدرج اللوني لأن tf.Tensor لا يتم "مراقبته" افتراضيًا ، tf.Variable غير قابل للتدريب:

# A trainable variable
x0 = tf.Variable(3.0, name='x0')
# Not trainable
x1 = tf.Variable(3.0, name='x1', trainable=False)
# Not a Variable: A variable + tensor returns a tensor.
x2 = tf.Variable(2.0, name='x2') + 1.0
# Not a variable
x3 = tf.constant(3.0, name='x3')

with tf.GradientTape() as tape:
  y = (x0**2) + (x1**2) + (x2**2)

grad = tape.gradient(y, [x0, x1, x2, x3])

for g in grad:
  print(g)
tf.Tensor(6.0, shape=(), dtype=float32)
None
None
None

يمكنك سرد المتغيرات التي يراقبها الشريط باستخدام طريقة GradientTape.watched_variables :

[var.name for var in tape.watched_variables()]
['x0:0']

tf.GradientTape يوفر خطافات تمنح المستخدم التحكم فيما يتم مشاهدته أو لا يتم مشاهدته.

لتسجيل التدرجات بالنسبة إلى tf.Tensor ، تحتاج إلى استدعاء GradientTape.watch(x) :

x = tf.constant(3.0)
with tf.GradientTape() as tape:
  tape.watch(x)
  y = x**2

# dy = 2x * dx
dy_dx = tape.gradient(y, x)
print(dy_dx.numpy())
6.0

على العكس من ذلك ، لتعطيل السلوك الافتراضي لمشاهدة جميع tf.Variables ، قم بتعيين watch_accessed_variables=False عند إنشاء شريط التدرج. يستخدم هذا الحساب متغيرين ، لكنه يربط فقط التدرج اللوني لأحد المتغيرات:

x0 = tf.Variable(0.0)
x1 = tf.Variable(10.0)

with tf.GradientTape(watch_accessed_variables=False) as tape:
  tape.watch(x1)
  y0 = tf.math.sin(x0)
  y1 = tf.nn.softplus(x1)
  y = y0 + y1
  ys = tf.reduce_sum(y)

نظرًا لعدم استدعاء GradientTape.watch على x0 ، لم يتم حساب أي تدرج فيما يتعلق به:

# dys/dx1 = exp(x1) / (1 + exp(x1)) = sigmoid(x1)
grad = tape.gradient(ys, {'x0': x0, 'x1': x1})

print('dy/dx0:', grad['x0'])
print('dy/dx1:', grad['x1'].numpy())
dy/dx0: None
dy/dx1: 0.9999546
نتائج متوسطة

يمكنك أيضًا طلب تدرجات المخرجات فيما يتعلق بالقيم الوسيطة المحسوبة داخل سياق tf.GradientTape .

x = tf.constant(3.0)

with tf.GradientTape() as tape:
  tape.watch(x)
  y = x * x
  z = y * y

# Use the tape to compute the gradient of z with respect to the
# intermediate value y.
# dz_dy = 2 * y and y = x ** 2 = 9
print(tape.gradient(z, y).numpy())
18.0

بشكل افتراضي ، يتم تحرير الموارد التي يحتفظ بها شريط GradientTape بمجرد استدعاء طريقة GradientTape.gradient . لحساب تدرجات متعددة على نفس الحساب ، قم بإنشاء شريط متدرج مع persistent=True . يسمح هذا باستدعاءات متعددة لطريقة gradient حيث يتم تحرير الموارد عندما يتم جمع كائن الشريط غير المرغوب فيه. فمثلا:

x = tf.constant([1, 3.0])
with tf.GradientTape(persistent=True) as tape:
  tape.watch(x)
  y = x * x
  z = y * y

print(tape.gradient(z, x).numpy())  # [4.0, 108.0] (4 * x**3 at x = [1.0, 3.0])
print(tape.gradient(y, x).numpy())  # [2.0, 6.0] (2 * x at x = [1.0, 3.0])
[  4. 108.]
[2. 6.]
del tape   # Drop the reference to the tape
ملاحظات على الأداء
  • هناك حمل صغير مرتبط بإجراء العمليات داخل سياق شريط التدرج. لن تكون هذه تكلفة ملحوظة في معظم عمليات التنفيذ الحثيثة ، ولكن لا يزال يتعين عليك استخدام سياق الشريط حول المناطق التي تكون مطلوبة فيها فقط.

  • تستخدم أشرطة التدرج الذاكرة لتخزين النتائج الوسيطة ، بما في ذلك المدخلات والمخرجات ، لاستخدامها أثناء التمرير للخلف.

    من أجل الكفاءة ، لا تحتاج بعض العمليات (مثل ReLU ) إلى الاحتفاظ بنتائجها الوسيطة ويتم تقليمها أثناء التمريرة الأمامية. ومع ذلك ، إذا كنت تستخدم persistent=True على الشريط الخاص بك ، فلن يتم تجاهل أي شيء وستكون ذروة استخدامك للذاكرة أعلى.

تدرجات الأهداف غير العددية

التدرج اللوني هو في الأساس عملية على سلمي.

x = tf.Variable(2.0)
with tf.GradientTape(persistent=True) as tape:
  y0 = x**2
  y1 = 1 / x

print(tape.gradient(y0, x).numpy())
print(tape.gradient(y1, x).numpy())
4.0
-0.25

وبالتالي ، إذا طلبت التدرج اللوني لأهداف متعددة ، فإن النتيجة لكل مصدر هي:

  • التدرج اللوني لمجموع الأهداف ، أو ما يعادله
  • مجموع تدرجات كل هدف.
x = tf.Variable(2.0)
with tf.GradientTape() as tape:
  y0 = x**2
  y1 = 1 / x

print(tape.gradient({'y0': y0, 'y1': y1}, x).numpy())
3.75

وبالمثل ، إذا لم يكن الهدف (الأهداف) عدديًا ، فسيتم حساب تدرج المجموع:

x = tf.Variable(2.)

with tf.GradientTape() as tape:
  y = x * [3., 4.]

print(tape.gradient(y, x).numpy())
7.0

هذا يجعل من السهل أخذ التدرج اللوني لمجموع مجموعة الخسائر ، أو التدرج اللوني لمجموع حساب الخسارة من حيث العنصر.

إذا كنت بحاجة إلى تدرج لوني منفصل لكل عنصر ، فارجع إلى اليعاقبة .

في بعض الحالات يمكنك تخطي اليعقوبي. لحساب العناصر ، يعطي التدرج اللوني المجموع مشتقًا لكل عنصر فيما يتعلق بعنصر الإدخال ، لأن كل عنصر مستقل:

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

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

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

بي إن جي

تدفق التحكم

نظرًا لأن شريط التدرج يسجل العمليات أثناء تنفيذها ، يتم التعامل مع تدفق التحكم في Python بشكل طبيعي (على سبيل المثال ، عبارات if and while ).

هنا يتم استخدام متغير مختلف في كل فرع من فروع if . لا يتصل التدرج اللوني إلا بالمتغير الذي تم استخدامه:

x = tf.constant(1.0)

v0 = tf.Variable(2.0)
v1 = tf.Variable(2.0)

with tf.GradientTape(persistent=True) as tape:
  tape.watch(x)
  if x > 0.0:
    result = v0
  else:
    result = v1**2 

dv0, dv1 = tape.gradient(result, [v0, v1])

print(dv0)
print(dv1)
tf.Tensor(1.0, shape=(), dtype=float32)
None

فقط تذكر أن عبارات التحكم نفسها غير قابلة للتفاضل ، لذا فهي غير مرئية لمحسّني التحسين المستند إلى التدرج اللوني.

اعتمادًا على قيمة x في المثال أعلاه ، إما أن يسجل الشريط result = v0 أو result = v1**2 . التدرج بالنسبة إلى x يكون دائمًا None .

dx = tape.gradient(result, x)

print(dx)
None
الحصول على تدرج None

عندما لا يكون الهدف متصلاً بمصدر ، ستحصل على تدرج None .

x = tf.Variable(2.)
y = tf.Variable(3.)

with tf.GradientTape() as tape:
  z = y * y
print(tape.gradient(z, x))
None

من الواضح أن z هنا غير متصل بـ x ، ولكن هناك عدة طرق أقل وضوحًا يمكن من خلالها فصل التدرج اللوني.

1. استبدال متغير بموتر

في قسم "التحكم في ما يشاهده الشريط" رأيت أن الشريط سيشاهد تلقائيًا tf.Variable ولكن ليس tf.Tensor .

أحد الأخطاء الشائعة هو استبدال متغير tf.Tensor tf.Variable بدلاً من استخدام Variable.assign لتحديث متغير tf.Variable . هنا مثال:

x = tf.Variable(2.0)

for epoch in range(2):
  with tf.GradientTape() as tape:
    y = x+1

  print(type(x).__name__, ":", tape.gradient(y, x))
  x = x + 1   # This should be `x.assign_add(1)`
ResourceVariable : tf.Tensor(1.0, shape=(), dtype=float32)
EagerTensor : None
2. إجراء حسابات خارج TensorFlow

لا يمكن للشريط تسجيل مسار التدرج إذا خرج الحساب من TensorFlow. فمثلا:

x = tf.Variable([[1.0, 2.0],
                 [3.0, 4.0]], dtype=tf.float32)

with tf.GradientTape() as tape:
  x2 = x**2

  # This step is calculated with NumPy
  y = np.mean(x2, axis=0)

  # Like most ops, reduce_mean will cast the NumPy array to a constant tensor
  # using `tf.convert_to_tensor`.
  y = tf.reduce_mean(y, axis=0)

print(tape.gradient(y, x))
None
3. أخذ التدرجات من خلال عدد صحيح أو سلسلة

الأعداد الصحيحة والسلاسل غير قابلة للتفاضل. إذا كان مسار الحساب يستخدم أنواع البيانات هذه ، فلن يكون هناك تدرج.

لا يتوقع أحد أن تكون السلاسل قابلة للتفاضل ، ولكن من السهل إنشاء ثابت أو متغير int عن طريق الخطأ إذا لم تحدد dtype .

x = tf.constant(10)

with tf.GradientTape() as g:
  g.watch(x)
  y = x * x

print(g.gradient(y, x))
WARNING:tensorflow:The dtype of the watched tensor must be floating (e.g. tf.float32), got tf.int32
WARNING:tensorflow:The dtype of the target tensor must be floating (e.g. tf.float32) when calling GradientTape.gradient, got tf.int32
WARNING:tensorflow:The dtype of the source tensor must be floating (e.g. tf.float32) when calling GradientTape.gradient, got tf.int32
None

لا يتم إرسال TensorFlow تلقائيًا بين الأنواع ، لذلك ، من الناحية العملية ، ستحصل غالبًا على خطأ في النوع بدلاً من التدرج اللوني المفقود.

4. أخذ التدرجات من خلال كائن ذي حالة

الدولة توقف التدرجات. عندما تقرأ من كائن ذي حالة ، يمكن للشريط ملاحظة الحالة الحالية فقط ، وليس التاريخ الذي يؤدي إليها.

tf.Tensor غير قابل للتغيير. لا يمكنك تغيير موتر بمجرد إنشائه. لها قيمة ولكن ليس لها دولة . جميع العمليات التي تمت مناقشتها حتى الآن هي أيضًا بدون حالة: إخراج tf.matmul يعتمد فقط على مدخلاته.

tf.Variable له حالة داخلية - قيمته. عند استخدام المتغير ، تتم قراءة الحالة. من الطبيعي حساب التدرج اللوني فيما يتعلق بالمتغير ، لكن حالة المتغير تمنع حسابات التدرج من العودة بعيدًا. فمثلا:

x0 = tf.Variable(3.0)
x1 = tf.Variable(0.0)

with tf.GradientTape() as tape:
  # Update x1 = x1 + x0.
  x1.assign_add(x0)
  # The tape starts recording from x1.
  y = x1**2   # y = (x1 + x0)**2

# This doesn't work.
print(tape.gradient(y, x0))   #dy/dx0 = 2*(x1 + x0)
None

وبالمثل ، فإن مكررات tf.data.Dataset و tf.queue s ذات حالة ، وستوقف كل التدرجات على الموترات التي تمر من خلالها.

لم يتم تسجيل التدرج

يتم تسجيل بعض tf.Operation على أنها غير قابلة للتفاضل وستعود None . آخرون ليس لديهم تدرج مسجل .

تعرض صفحة tf.raw_ops العمليات ذات المستوى المنخفض التي تم تسجيل التدرجات اللونية لها.

إذا حاولت أن تأخذ تدرجًا من خلال عملية تعويم لا تحتوي على تدرج مسجّل ، فإن الشريط سيرمي خطأً بدلاً من إرجاع None بصمت. بهذه الطريقة تعرف أن شيئًا ما قد حدث خطأ.

على سبيل المثال ، تعمل الدالة tf.image.adjust_contrast على التفاف raw_ops.AdjustContrastv2 ، والتي يمكن أن تحتوي على تدرج ولكن لم يتم تنفيذ التدرج اللوني:

image = tf.Variable([[[0.5, 0.0, 0.0]]])
delta = tf.Variable(0.1)

with tf.GradientTape() as tape:
  new_image = tf.image.adjust_contrast(image, delta)

try:
  print(tape.gradient(new_image, [image, delta]))
  assert False   # This should not happen.
except LookupError as e:
  print(f'{type(e).__name__}: {e}')
LookupError: gradient registry has no entry for: AdjustContrastv2

إذا كنت بحاجة إلى التفريق من خلال هذا المرجع ، فستحتاج إما إلى تنفيذ التدرج اللوني وتسجيله (باستخدام tf.RegisterGradient ) أو إعادة تنفيذ الوظيفة باستخدام عمليات أخرى.

أصفار بدلاً من لا شيء

في بعض الحالات ، قد يكون من الملائم الحصول على 0 بدلاً من None للتدرجات غير المتصلة. يمكنك تحديد ما تريد إرجاعه عندما يكون لديك تدرجات غير متصلة باستخدام وسيطة unconnected_gradients :

x = tf.Variable([2., 2.])
y = tf.Variable(3.)

with tf.GradientTape() as tape:
  z = y**2
print(tape.gradient(z, x, unconnected_gradients=tf.UnconnectedGradients.ZERO))
tf.Tensor([0. 0.], shape=(2,), dtype=float32)