<jupyter_start><jupyter_text>Gradient Tape BasicsIn this ungraded lab, you'll get familiar with Tensorflow's built in API called Gradient Tape which helps in performing automatic differentiation.  Imports<jupyter_code>import tensorflow as tf<jupyter_output><empty_output><jupyter_text>Exercise on basics of Gradient TapeLet's explore how you can use [tf.GradientTape()](https://www.tensorflow.org/api_docs/python/tf/GradientTape) to do automatic differentiation.<jupyter_code># Define a 2x2 array of 1's
x = tf.ones((2,2))

with tf.GradientTape() as t:
    # Record the actions performed on tensor x with `watch`
    t.watch(x) 

    # Define y as the sum of the elements in x
    y =  tf.reduce_sum(x)

    # Let z be the square of y
    z = tf.square(y) 

# Get the derivative of z wrt the original input tensor x
dz_dx = t.gradient(z, x)

# Print our result
print(dz_dx)<jupyter_output>tf.Tensor(
[[8. 8.]
 [8. 8.]], shape=(2, 2), dtype=float32)<jupyter_text>Gradient tape expires after one use, by defaultIf you want to compute multiple gradients, note that by default, GradientTape is not persistent (`persistent=False`).  This means that the GradientTape will expire after you use it to calculate a gradient.To see this, set up gradient tape as usual and calculate a gradient, so that the gradient tape will be 'expired'.<jupyter_code>x = tf.constant(3.0)

# Notice that persistent is False by default
with tf.GradientTape() as t:
    t.watch(x)
    
    # y = x^2
    y = x * x
    
    # z = y^2
    z = y * y

# Compute dz/dx. 4 * x^3 at x = 3 --> 108.0
dz_dx = t.gradient(z, x)
print(dz_dx)<jupyter_output>tf.Tensor(108.0, shape=(), dtype=float32)<jupyter_text>Gradient tape has expiredSee what happens if you try to calculate another gradient after you've already used gradient tape once.<jupyter_code># If you try to compute dy/dx after the gradient tape has expired:
try:
    dy_dx = t.gradient(y, x)  # 6.0
    print(dy_dx)
except RuntimeError as e:
    print("The error message you get is:")
    print(e)<jupyter_output>The error message you get is:
GradientTape.gradient can only be called once on non-persistent tapes.<jupyter_text>Make the gradient tape persistentTo make sure that the gradient tape can be used multiple times, set `persistent=True`<jupyter_code>x = tf.constant(3.0)

# Set persistent=True so that you can reuse the tape
with tf.GradientTape(persistent=True) as t:
    t.watch(x)
    
    # y = x^2
    y = x * x
    
    # z = y^2
    z = y * y

# Compute dz/dx. 4 * x^3 at x = 3 --> 108.0
dz_dx = t.gradient(z, x)
print(dz_dx)<jupyter_output>tf.Tensor(108.0, shape=(), dtype=float32)<jupyter_text>Now that it's persistent, you can still reuse this tape!Try calculating a second gradient on this persistent tape.<jupyter_code># You can still compute dy/dx because of the persistent flag.
dy_dx = t.gradient(y, x)  # 6.0
print(dy_dx)<jupyter_output>tf.Tensor(6.0, shape=(), dtype=float32)<jupyter_text>Great! It still works!  Delete the tape variable `t` once you no longer need it.<jupyter_code># Drop the reference to the tape
del t<jupyter_output><empty_output><jupyter_text>Nested Gradient tapesNow let's try computing a higher order derivative by nesting the `GradientTapes:` Acceptable indentation of the first gradient calculationKeep in mind that you'll want to make sure that the first gradient calculation of `dy_dx` should occur at least inside the outer `with` block.<jupyter_code>x = tf.Variable(1.0)

with tf.GradientTape() as tape_2:
    with tf.GradientTape() as tape_1:
        y = x * x * x
    
    # The first gradient calculation should occur at leaset
    # within the outer with block
    dy_dx = tape_1.gradient(y, x)
d2y_dx2 = tape_2.gradient(dy_dx, x)

print(dy_dx)
print(d2y_dx2)<jupyter_output>tf.Tensor(3.0, shape=(), dtype=float32)
tf.Tensor(6.0, shape=(), dtype=float32)<jupyter_text>The first gradient calculation can also be inside the inner with block.<jupyter_code>x = tf.Variable(1.0)

with tf.GradientTape() as tape_2:
    with tf.GradientTape() as tape_1:
        y = x * x * x
    
        # The first gradient calculation can also be within the inner with block
        dy_dx = tape_1.gradient(y, x)
d2y_dx2 = tape_2.gradient(dy_dx, x)

print(dy_dx)
print(d2y_dx2)<jupyter_output>tf.Tensor(3.0, shape=(), dtype=float32)
tf.Tensor(6.0, shape=(), dtype=float32)<jupyter_text>Where not to indent the first gradient calculationIf the first gradient calculation is OUTSIDE of the outer `with` block, it won't persist for the second gradient calculation.<jupyter_code>x = tf.Variable(1.0)

with tf.GradientTape() as tape_2:
    with tf.GradientTape() as tape_1:
        y = x * x * x

# The first gradient call is outside the outer with block
# so the tape will expire after this
dy_dx = tape_1.gradient(y, x)

# The tape is now expired and the gradient output will be `None`
d2y_dx2 = tape_2.gradient(dy_dx, x)

print(dy_dx)
print(d2y_dx2)<jupyter_output>tf.Tensor(3.0, shape=(), dtype=float32)
None<jupyter_text>Notice how the `d2y_dx2` calculation is now `None`.  The tape has expired.  Also note that this still won't work even if you set persistent=True for both gradient tapes.<jupyter_code>x = tf.Variable(1.0)

# Setting persistent=True still won't work
with tf.GradientTape(persistent=True) as tape_2:
    # Setting persistent=True still won't work
    with tf.GradientTape(persistent=True) as tape_1:
        y = x * x * x

# The first gradient call is outside the outer with block
# so the tape will expire after this
dy_dx = tape_1.gradient(y, x)

# the output will be `None`
d2y_dx2 = tape_2.gradient(dy_dx, x)

print(dy_dx)
print(d2y_dx2)<jupyter_output>tf.Tensor(3.0, shape=(), dtype=float32)
None<jupyter_text>Proper indentation for the second gradient calculationThe second gradient calculation `d2y_dx2` can be indented as much as the first calculation of `dy_dx` but not more.<jupyter_code>x = tf.Variable(1.0)

with tf.GradientTape() as tape_2:
    with tf.GradientTape() as tape_1:
        y = x * x * x

        dy_dx = tape_1.gradient(y, x)
        
        # this is acceptable
        d2y_dx2 = tape_2.gradient(dy_dx, x)

print(dy_dx)
print(d2y_dx2)<jupyter_output>tf.Tensor(3.0, shape=(), dtype=float32)
tf.Tensor(6.0, shape=(), dtype=float32)<jupyter_text>This is also acceptable<jupyter_code>x = tf.Variable(1.0)

with tf.GradientTape() as tape_2:
    with tf.GradientTape() as tape_1:
        y = x * x * x

        dy_dx = tape_1.gradient(y, x)
        
    # this is also acceptable
    d2y_dx2 = tape_2.gradient(dy_dx, x)

print(dy_dx)
print(d2y_dx2)<jupyter_output>tf.Tensor(3.0, shape=(), dtype=float32)
tf.Tensor(6.0, shape=(), dtype=float32)<jupyter_text>This is also acceptable<jupyter_code>x = tf.Variable(1.0)

with tf.GradientTape() as tape_2:
    with tf.GradientTape() as tape_1:
        y = x * x * x

        dy_dx = tape_1.gradient(y, x)
        
# this is also acceptable
d2y_dx2 = tape_2.gradient(dy_dx, x)

print(dy_dx)
print(d2y_dx2)<jupyter_output>tf.Tensor(3.0, shape=(), dtype=float32)
tf.Tensor(6.0, shape=(), dtype=float32)
{'fraction_non_alphanumeric': 0.10417258018734034, 'fraction_numerical': 0.025120635821742832, 'mean_word_length': 4.147552958363769, 'pattern_counts': {'":': 0, '<': 45, '<?xml version=': 0, '>': 47, 'https://': 1, 'lorem ipsum': 0, 'www.': 1, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '14', 'dir': 'jupyter-structured-clean-dedup', 'id': '129872', 'license': 'MIT', 'n_tokens_mistral': 2609, 'n_tokens_neox': 2462, 'n_words': 791, 'path': 'C2W1_L2_GradientTapeBasics.ipynb', 'repo_name': '100rab-S/TensorFlow-Advanced-Techniques', 'size': '7046'}