代做GR5242 HW03 Problem 4: Automatic Differentiation代做R编程

2024-11-19 代做GR5242 HW03 Problem 4: Automatic Differentiation代做R编程

GR5242 HW03 Problem 4: Automatic Differentiation

Instructions: This problem is an individual assignment -- you are to complete this problem on your own, without conferring with your classmates. You should submit a completed and published notebook to Courseworks; no other files will be accepted.

Description:

This homework adds more detail to the autodiff lecture in class. There are seven questions (30 points) in total, which include coding and written questions. You can only modify the codes and text within ### YOUR CODE HERE ### and/or ### YOUR ANSWER HERE ###.

In [ ]: import torch import matplotlib.pyplot as plt import numpy as np from datetime import datetime import sympy as sym

Differentiation

Different ways of differentiation: In this problem we consider a particular function (which we call it an op in the code block, short for operation)  f : R → R : x ↦ 4x(1 − x) and it's compositions with itself. Let us define,

where f (n)(x) = f(f(n−1)(x)) is the n times composited function f with itself. The goal of this problem is to explore various ways of differentiation, i.e. dx/d gn(x) which are listed below:

Numerical differentiation

Symbolic differentiation

Autodiff in forward or reverse mode

In [ ]: def op(inp): return 4 * inp * ( 1 - inp ) def operation(inp, n = 1): temp = inp for i in range(n): temp = op(temp) # apply this operation n times return temp

Numerical Differentiation

Based on the definition of derivative at a particular point x0,

The formula above suggests a simple way of approximating the derivative by taking Dhgn(x0) for a particular choice of h as your approximate derivative. This is also known as the finite difference method. Note that this approach only requires evaluation of your function around the point you are trying to take the derivative at so it's computationally efficient but the caveat is that choosing a proper h to obtain good enough approximations is generally hard (specially when the function is multivariate). However, in our case we have some structure over the function we are trying to take derivative of.

Question 1 (3 points): Use a finite difference with tolerance 1e-12 to approximate the derivative of at g3(x) at x = 1:

In [ ]: tol = 1e-12 ### Your Code Here ###

The approximation will be bad at certain points x, and becomes less stable as n becomes larger for a fixed h (or as the dimensionality of the function grows).

Calculus and (manual/symbolic) differentiation

Notice that f is a polynomial and that composition of polynomials yields also a polynomial. Therefore, gn is a polynomial with degree 2n  (try to argue this for yourself using induction); we can compute the derivative using calculus.

Instead of computing the derivative by hand we use the help of an auxillary package sympy and try to compute the derivative. This package uses symbolic variables and traces operations such as add, multiplication, division, etc., applied onto these variables and computes the derivative using chain rule.

It is not difficult to (manually) derive a closed form. expression for gn using the recursive formula

gn (x) = 4gn−1(x)(1 − gn−1(x)),  g1 (x) = 4x(1 − x).

The following block of code prints this closed form. expression in terms of a symbolic variable X.

In [ ]: X = sym.Symbol('X') # Create a symbolic variable sym.init_printing(use_unicode=False, wrap_line=True)

Now that we have the closed form. expression, we may use the chain rule to express the derivative of gn.

In [ ]: dydx_symbolic = sym.diff(Y, X) dydx_symbolic Out[ ]: 64X (1 − X)(8X − 4)(−16X (1 − X)(−4X (1 − X) + 1) + 1) + 64X (1 − X)(−4X (1 − X) + 1)(−16X (1 − X)(8X − 4) + 16X (−4X (1 − X) + 1) ⋅ (1 − X)(−4X (1 − X) + 1)) − 64X (−4X (1 − X) + 1)(−16X (1 − X)(−4X (1 − X) ⋅ (1 − X)(−4X (1 − X) + 1)(−16X (1 − X)(−4X (1 − X) + 1) + 1)

Question 2 (3 points): Compute the exact derivative of at by evaluating the symbolic expression.

In [ ]: # hint: read about subs/evalf here: https://docs.sympy.org/latest/modules/numer ### Your Code Here ###

As it is evident the closed form. expression for dydx_symbolic is unwieldy. We can make it more efficient by expanding and collecting all the terms. The goal is to represent the derivative as a natural polynomial. Yet this is another way of computing the derivative.

Question 3 (3 points): Using the sympy documentation page expand/simplify the closed form. expression of gn(x) and print its derivative as a symbolic expression.

In [ ]: ### Your Code Here ### polynomial = ... dydx_symbolic_simplified = ... dydx_symbolic_simplified # This should express a polynomial of degree 2^n - 1.

Ellipsis

Out[ ]:

Now that we have a compact closed form. for the derivative we can evaluate it efficiently at various values of x.

Question 4 (2 points): Evaluate dydx_symbolic_simplified at x = 1.

In [ ]: ### Your Code Here ###

Chain Rule and Autodiff

As we've seen in the previous part, symbolic engines computes the derivative using some closed form. expression of the function. In particular, sympy simplified the recursive operation and expressed the function gn in terms of basic operations over the symbolic variable X. On the other hand, Autodiff does not necessarily simplify these operations and apply chain rule directly to the latest operation either in forward or backward mode.

Question 5 (4 points): Using the recursive representation gn (x) = f(gn−1(x)) calculate the derivative of gn as a function of gn−1 and g'n−1. (To be clear, this part is not code; write the mathematical expression below)

'### Your Answer Here ###'

This implies that we can compute g'n(x) in the forward mode if we could compute gn−1(x) and g'n−1 (x) in the forward mode. Indeed, this is possible by applying the same logic to g'n−1 (x) and compute it based on gn−2(x) and g'n−2 (x); In other words, by augmenting the forward computation graph g1 → g2 ⋯ → gn with (g1,g'1) → (g2,g'2), ⋯ → (gn,g'n) we can compute the derivative in a forward pass.

Question 6 (8 points): Modify the functions  op  and  operation with their counterparts op_with_grad  and  operation_with_grad  using the logic explained above to compute the derivative of gn. Check your function by evaluating it atx = 1.

In [ ]: ### Your Code Here ### def op_with_grad(inp, grad): # inp could be any function of x. # grad is the corresponsing derivative of inp with respect to x. # The function should return a tuple where the first element is the `op` appl # and the second element should be the gradient of op(inp) with respect to x. pass def operation_with_grad(inp, n = 1): # inp represents x here. # This function should output a tuple where the first element is the value of # and the second element should be the derivative evaluated at inp, i.e. g_n^ pass operation_with_grad(1.0, n = 3)

Fortunately, torch can do last part for us! As was discussed in class, torch interprets our code and builds up a computation graph using operations that knows their gradients already and complements each operation with their backward gradient; in order to compute the gradient it follows the following backwards path

where g0(x) = x is the identity function.

Question 7 (7 points): Use PyTorch to calculate the derivative of g_n(x) with respect to x

In [ ]: def torch_op(inp): return 4 * inp * ( 1 - inp ) def torch_operation(inp, n = 1): temp = inp for i in range(n): temp = torch_op(temp) # apply this operation n times return temp In [ ]: x = torch.tensor(1.0, requires_grad=True) #### Your code here ##### # define the function with n=3 iterations as before, and use pytorch to autodif df_dx =... print(df_dx.numpy()) In [ ]: