ความรู้เบื้องต้นเกี่ยวกับการไล่ระดับสีและการสร้างความแตกต่างอัตโนมัติ

ดูบน TensorFlow.org ทำงานใน Google Colab ดูแหล่งที่มาบน GitHub ดาวน์โหลดโน๊ตบุ๊ค

การแยกความแตกต่างและการไล่ระดับสีอัตโนมัติ

การ แยกความแตกต่างอัตโนมัติ มีประโยชน์สำหรับการนำอัลกอริธึมการเรียนรู้ของเครื่องมาใช้ เช่น การขยาย พันธุ์ย้อนหลัง สำหรับเครือข่ายประสาทเทียมในการฝึกอบรม

ในคู่มือนี้ คุณจะสำรวจวิธีการคำนวณการไล่ระดับสีด้วย TensorFlow โดยเฉพาะอย่างยิ่งใน การดำเนินการอย่างกระตือรือร้น

ติดตั้ง

import numpy as np
import matplotlib.pyplot as plt

import tensorflow as tf

การคำนวณการไล่ระดับสี

เพื่อแยกความแตกต่างโดยอัตโนมัติ TensorFlow จำเป็นต้องจำการดำเนินการที่เกิดขึ้นในลำดับใดระหว่างการ ส่ง ต่อ จากนั้น ระหว่างการ ย้อนกลับ TensorFlow จะข้ามรายการการดำเนินการนี้ในลำดับย้อนกลับเพื่อคำนวณการไล่ระดับสี

เทปไล่โทนสี

TensorFlow จัดเตรียม tf.GradientTape API สำหรับการแยกความแตกต่างโดยอัตโนมัติ นั่นคือ การคำนวณเกรเดียนต์ของการคำนวณที่เกี่ยวกับอินพุตบางตัว ปกติแล้ว tf.Variable s TensorFlow "บันทึก" การดำเนินการที่เกี่ยวข้องซึ่งดำเนินการภายในบริบทของ tf.GradientTape ลงบน "เทป" จากนั้น TensorFlow จะใช้เทปนั้นเพื่อคำนวณการไล่ระดับของการคำนวณที่ "บันทึกไว้" โดยใช้ โหมด Reverse mode differentiation

นี่เป็นตัวอย่างง่ายๆ:

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 หรือหนึ่งในคลาสย่อย ( layers.Layer , keras.Model ) สำหรับ จุดตรวจสอบ และการ ส่งออก

ในกรณีส่วนใหญ่ คุณจะต้องคำนวณการไล่ระดับสีตามตัวแปรที่ฝึกได้ของแบบจำลอง เนื่องจากคลาสย่อยทั้งหมดของ 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
ตัวยึดตำแหน่ง22

ผลลัพธ์ระดับกลาง

คุณยังสามารถขอการไล่ระดับสีของผลลัพธ์ที่เกี่ยวข้องกับค่ากลางที่คำนวณภายในบริบท 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
ตัวยึดตำแหน่ง33

ซึ่งทำให้ง่ายต่อการใช้การไล่ระดับของผลรวมของคอลเลกชันของการสูญเสีย หรือการไล่ระดับของผลรวมของการคำนวณการสูญเสียตามองค์ประกอบ

หากคุณต้องการการไล่ระดับสีแยกกันสำหรับแต่ละรายการ ให้อ้างอิงกับ จาโคเบียน

ในบางกรณี คุณสามารถข้ามภาษาจาโคเบียนได้ สำหรับการคำนวณตามองค์ประกอบ การไล่ระดับของผลรวมจะให้อนุพันธ์ของแต่ละองค์ประกอบเทียบกับองค์ประกอบอินพุต เนื่องจากแต่ละองค์ประกอบเป็นอิสระจากกัน:

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

png

ควบคุมการไหล

เนื่องจากเทปเกรเดียนต์บันทึกการดำเนินการขณะที่ดำเนินการ โฟลว์การควบคุม Python จะได้รับการจัดการอย่างเป็นธรรมชาติ (เช่น คำสั่ง if และ 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
ตัวยึดตำแหน่ง39

รับการไล่ระดับของ None

เมื่อเป้าหมายไม่ได้เชื่อมต่อกับแหล่งที่มา คุณจะได้รับการไล่ระดับสี None

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

with tf.GradientTape() as tape:
  z = y * y
print(tape.gradient(z, x))
None
ตัวยึดตำแหน่ง41

เห็นได้ชัดว่า z ไม่ได้เชื่อมต่อกับ x แต่มีหลายวิธีที่ไม่ชัดเจนที่สามารถตัดการไล่ระดับสีได้

1. แทนที่ตัวแปรด้วยเทนเซอร์

ในส่วน "การควบคุมสิ่งที่ดูเทป" คุณเห็นว่าเทปจะดูโดยอัตโนมัติ tf.Variable แต่ไม่ใช่ tf.Tensor

ข้อผิดพลาดทั่วไปอย่างหนึ่งคือการแทนที่ tf.Variable ด้วย tf.Tensor โดยไม่ได้ตั้งใจ แทนที่จะใช้ 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
ตัวยึดตำแหน่ง43

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 iterators และ tf.queue s เป็น stateful และจะหยุดการไล่ระดับทั้งหมดบนเทนเซอร์ที่ส่งผ่าน

ไม่มีการไล่ระดับสีที่ลงทะเบียน

tf.Operation บางรายการได้รับ การลงทะเบียนว่าไม่แตกต่าง และจะส่งกลับ None อื่นๆ ไม่มีการไล่ระดับสีที่ลงทะเบียน

หน้า tf.raw_ops แสดงว่า ops ระดับต่ำใดที่มีการไล่ระดับสีที่ลงทะเบียนไว้

หากคุณพยายามใช้การไล่ระดับสีผ่าน float op ที่ไม่มีการไล่ระดับสีที่ลงทะเบียนไว้ เทปจะแสดงข้อผิดพลาดแทนที่จะส่งคืน 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
ตัวยึดตำแหน่ง51

หากคุณต้องการแยกความแตกต่างจาก op นี้ คุณจะต้องใช้การไล่ระดับสีและลงทะเบียน (โดยใช้ tf.RegisterGradient ) หรือใช้งานฟังก์ชันใหม่โดยใช้ ops อื่น

ศูนย์แทน None

ในบางกรณี จะเป็นการสะดวกที่จะได้รับ 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)
ตัวยึดตำแหน่ง53